﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="Expando.cs" company="Skymate">
//   Copyright @2015 Skymate
// </copyright>
// <summary>
//   Class that provides extensible properties and methods to an
//   existing object when cast to dynamic. This
//   dynamic object stores 'extra' properties in a dictionary or
//   checks the actual properties of the instance passed via
//   constructor.
//   This class can be subclassed to extend an existing type or
//   you can pass in an instance to extend. Properties (both
//   dynamic and strongly typed) can be accessed through an
//   indexer.
//   This type allows you three ways to access its properties:
//   Directly: any explicitly declared properties are accessible
//   Dynamic: dynamic cast allows access to dictionary and native properties/methods
//   Dictionary: Any of the extended properties are accessible via IDictionary interface
// </summary>
// --------------------------------------------------------------------------------------------------------------------

#region License

/*
 **************************************************************
 *  Author: Rick Strahl 
 *          © West Wind Technologies, 2012
 *          http://www.west-wind.com/
 * 
 * Created: Feb 2, 2012
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 **************************************************************  
*/
#endregion

namespace Skymate.ComponentModel
{
    using System;
    using System.Collections.Generic;
    using System.Dynamic;
    using System.Linq;
    using System.Reflection;

    using Fasterflect;

    /// <summary>
    ///     Class that provides extensible properties and methods to an
    ///     existing object when cast to dynamic. This
    ///     dynamic object stores 'extra' properties in a dictionary or
    ///     checks the actual properties of the valueInstance passed via
    ///     constructor.
    ///     This class can be subclassed to extend an existing type or
    ///     you can pass in an valueInstance to extend. Properties (both
    ///     dynamic and strongly typed) can be accessed through an
    ///     indexer.
    ///     This type allows you three ways to access its properties:
    ///     Directly: any explicitly declared properties are accessible
    ///     Dynamic: dynamic cast allows access to dictionary and native properties/methods
    ///     Dictionary: Any of the extended properties are accessible via IDictionary interface
    /// </summary>
    [Serializable]
    public sealed class Expando : DynamicObject
    {
        #region Fields

        /// <summary>
        ///     String Dictionary that contains the extra dynamic values
        ///     stored on this object/valueInstance
        /// </summary>
        /// <remarks>Using PropertyBag to support XML Serialization of the dictionary</remarks>
        private readonly PropertyBag properties = new PropertyBag();

        /// <summary>
        ///     Instance of object passed in
        /// </summary>
        private object instance;

        /// <summary>
        ///     The _instance property info.
        /// </summary>
        private IList<PropertyInfo> instancePropertyInfo;

        /// <summary>
        ///     Cached type of the valueInstance
        /// </summary>
        private Type instanceType;
        
        #endregion

        #region Constructors

        /// <summary>
        ///     Initializes a new valueInstance of the <see cref="Expando" /> class.
        ///     This constructor just works off the internal dictionary and any
        ///     public properties of this object.
        ///     Note you can subclass Expando.
        /// </summary>
        public Expando()
        {
            this.Initialize(this);
        }

        /// <summary>
        /// Initializes a new valueInstance of the <see cref="Expando"/> class.
        ///     Allows passing in an existing valueInstance variable to 'extend'.
        /// </summary>
        /// <remarks>
        /// You can pass in null here if you don't want to
        ///     check native properties and only check the Dictionary!
        /// </remarks>
        /// <param name="instance">
        /// 实例
        /// </param>
        public Expando(object instance)
        {
            this.Initialize(instance);
        }

        #endregion

        #region Properties

        /// <summary>
        ///     Gets the valueInstance property info.
        /// </summary>
        private IList<PropertyInfo> InstancePropertyInfo
        {
            get
            {
                if (this.instancePropertyInfo == null && this.instance != null)
                {
                    this.instancePropertyInfo = this.instance.GetType().Properties(Flags.InstancePublicDeclaredOnly);
                }

                return this.instancePropertyInfo;
            }
        }

        #endregion

        #region Indexs

        /// <summary>
        /// Convenience method that provides a string Indexer
        ///     to the Properties collection AND the strongly typed
        ///     properties of the object by name.
        ///     // dynamic
        ///     exp["Address"] = "112 nowhere lane";
        ///     // strong
        ///     var name = exp["StronglyTypedProperty"] as string;
        /// </summary>
        /// <remarks>
        /// The getter checks the Properties dictionary first
        ///     then looks in PropertyInfo for properties.
        ///     The setter checks the valueInstance properties before
        ///     checking the Properties dictionary.
        /// </remarks>
        /// <param name="key">
        /// 关键字
        /// </param>
        /// <returns>
        /// The <see cref="object"/>.
        /// </returns>
        public object this[string key]
        {
            get
            {
                try
                {
                    // try to get from properties collection first
                    return this.properties[key];
                }
                catch (KeyNotFoundException)
                {
                    // try reflection on instanceType
                    object result;
                    if (this.GetProperty(this.instance, key, out result))
                    {
                        return result;
                    }

                    // no doesn't exist
                    throw;
                }
            }

            set
            {
                if (this.properties.ContainsKey(key))
                {
                    this.properties[key] = value;
                    return;
                }

                // check valueInstance for existance of type first
                var pi = this.instanceType.Property(key, Flags.Public);
                if (pi != null)
                {
                    this.SetProperty(this.instance, key, value);
                }
                else
                {
                    this.properties[key] = value;
                }
            }
        }

        #endregion

        #region Methods

        /// <summary>
        ///     The get dynamic member names.
        /// </summary>
        /// <returns>
        ///     The <see>
        ///         <cref>IEnumerable</cref>
        ///     </see>
        ///     .
        /// </returns>
        public override IEnumerable<string> GetDynamicMemberNames()
        {
            return this.GetProperties().Select(prop => prop.Key);
        }

        /// <summary>
        /// Try to retrieve a member by name first from valueInstance properties
        ///     followed by the collection entries.
        /// </summary>
        /// <param name="binder">
        /// binder
        /// </param>
        /// <param name="result">
        /// result
        /// </param>
        /// <returns>
        /// The <see cref="bool"/>.
        /// </returns>
        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            // first check the Properties collection for member
            if (this.properties.Keys.Contains(binder.Name))
            {
                result = this.properties[binder.Name];
                return true;
            }

            // Next check for Public properties via Reflection
            if (this.instance != null)
            {
                try
                {
                    return this.GetProperty(this.instance, binder.Name, out result);
                }
                catch
                {
                    // ignored
                }
            }

            // failed to retrieve a property
            result = null;
            return false;
        }

        /// <summary>
        /// Property setter implementation tries to retrieve value from valueInstance
        ///     first then into this object
        /// </summary>
        /// <param name="binder">
        /// binder
        /// </param>
        /// <param name="value">
        /// value
        /// </param>
        /// <returns>
        /// The <see cref="bool"/>.
        /// </returns>
        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            // first check to see if there's a native property to set
            if (this.instance != null)
            {
                try
                {
                    var result = this.SetProperty(this.instance, binder.Name, value);
                    if (result)
                    {
                        return true;
                    }
                }
                catch
                {
                    // ignored
                }
            }

            // no match - set or add to dictionary
            this.properties[binder.Name] = value;
            return true;
        }

        /// <summary>
        /// Dynamic invocation method. Currently allows only for Reflection based
        ///     operation (no ability to add methods dynamically).
        /// </summary>
        /// <param name="binder">
        /// binder
        /// </param>
        /// <param name="args">
        /// 参数
        /// </param>
        /// <param name="result">
        /// 结果
        /// </param>
        /// <returns>
        /// The <see cref="bool"/>.
        /// </returns>
        public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
        {
            if (this.instance != null)
            {
                try
                {
                    // check valueInstance passed in for methods to invoke
                    if (this.InvokeMethod(this.instance, binder.Name, args, out result))
                    {
                        return true;
                    }
                }
                catch
                {
                    // ignored
                }
            }

            result = null;
            return false;
        }

        /// <summary>
        /// Returns and the properties of
        /// </summary>
        /// <param name="includeInstanceProperties">
        /// The include Instance Properties.
        /// </param>
        /// <returns>
        /// The <see>
        ///         <cref>IEnumerable</cref>
        ///     </see>
        ///     .
        /// </returns>
        public IEnumerable<KeyValuePair<string, object>> GetProperties(bool includeInstanceProperties = false)
        {
            if (includeInstanceProperties && this.instance != null)
            {
                foreach (var prop in this.InstancePropertyInfo)
                {
                    yield return new KeyValuePair<string, object>(prop.Name, prop.GetValue(this.instance, null));
                }
            }

            foreach (var key in this.properties.Keys)
            {
                yield return new KeyValuePair<string, object>(key, this.properties[key]);
            }
        }

        /// <summary>
        /// Checks whether a property exists in the Property collection
        ///     or as a property on the valueInstance
        /// </summary>
        /// <param name="item">
        /// 项目
        /// </param>
        /// <param name="includeInstanceProperties">
        /// The include Instance Properties.
        /// </param>
        /// <returns>
        /// The <see>
        ///         <cref>bool</cref>
        ///     </see>
        ///     .
        /// </returns>
        public bool Contains(KeyValuePair<string, object> item, bool includeInstanceProperties = false)
        {
            return this.Contains(item.Key, includeInstanceProperties);
        }

        /// <summary>
        /// Checks whether a property exists in the Property collection
        ///     or as a property on the valueInstance
        /// </summary>
        /// <param name="propertyName">
        /// 属性名
        /// </param>
        /// <param name="includeInstanceProperties">
        /// 属性是否包含
        /// </param>
        /// <returns>
        /// The <see cref="bool"/>.
        /// </returns>
        public bool Contains(string propertyName, bool includeInstanceProperties = false)
        {
            var res = this.properties.ContainsKey(propertyName);
            if (res)
            {
                return true;
            }

            if (includeInstanceProperties && this.instance != null)
            {
                foreach (var prop in this.InstancePropertyInfo)
                {
                    if (prop.Name == propertyName)
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// The try get value.
        /// </summary>
        /// <param name="propertyName">
        /// The property name.
        /// </param>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// The <see cref="bool"/>.
        /// </returns>
        public bool TryGetValue(string propertyName, out object value)
        {
            value = null;

            if (this.Contains(propertyName, true))
            {
                value = this[propertyName];
                return true;
            }

            return false;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Reflection Helper method to retrieve a property
        /// </summary>
        /// <param name="valueInstance">
        /// 实例
        /// </param>
        /// <param name="name">
        /// 名称
        /// </param>
        /// <param name="result">
        /// 结果
        /// </param>
        /// <returns>
        /// The <see cref="bool"/>.
        /// </returns>
        private bool GetProperty(object valueInstance, string name, out object result)
        {
            if (valueInstance == null)
            {
                valueInstance = this;
            }

            var pi = this.instanceType.Property(name, Flags.InstancePublic);
            if (pi != null)
            {
                result = valueInstance.GetPropertyValue(pi.Name);
                return true;
            }

            result = null;
            return false;
        }

        /// <summary>
        /// The initialize.
        /// </summary>
        /// <param name="valueInstance">
        /// The valueInstance.
        /// </param>
        private void Initialize(object valueInstance)
        {
            this.instance = valueInstance;
            if (valueInstance != null)
            {
                this.instanceType = valueInstance.GetType();
            }
        }

        /// <summary>
        /// Reflection helper method to set a property value
        /// </summary>
        /// <param name="objectValue">
        /// 实例
        /// </param>
        /// <param name="name">
        /// 名称
        /// </param>
        /// <param name="value">
        /// 值
        /// </param>
        /// <returns>
        /// The <see cref="bool"/>.
        /// </returns>
        private bool SetProperty(object objectValue, string name, object value)
        {
            if (objectValue == null)
            {
                objectValue = this;
            }

            var pi = this.instanceType.Property(name, Flags.InstancePublic);
            if (pi != null)
            {
                objectValue.SetPropertyValue(pi.Name, value);
                return true;
            }

            return false;
        }

        /// <summary>
        /// Reflection helper method to invoke a method
        /// </summary>
        /// <param name="objectVaue">
        /// 对象实例
        /// </param>
        /// <param name="name">
        /// 属性名称
        /// </param>
        /// <param name="args">
        /// 参数
        /// </param>
        /// <param name="result">
        /// 结果
        /// </param>
        /// <returns>
        /// The <see cref="bool"/>.
        /// </returns>
        private bool InvokeMethod(object objectVaue, string name, object[] args, out object result)
        {
            if (objectVaue == null)
            {
                objectVaue = this;
            }

            // Look at the instanceType
            var mi = this.instanceType.Method(name, Flags.InstancePublic);

            if (mi != null)
            {
                result = objectVaue.CallMethod(mi.Name, args);
                return true;
            }

            result = null;
            return false;
        }

        #endregion
    }
}